/*
 * Copyright 2009 (c) Patrick Pietens.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */
package indigo.service.services 
{	import indigo.service.events.ServiceEvent;
	import indigo.utils.Base64;

	import flash.events.Event;
	import flash.net.URLRequestMethod;
	import flash.utils.ByteArray;
	/**
	 * The XMLRPCService class is used to send an load data to an XMLRPC remote service. 
	 * It is recommend to use the sendAndLoadXMLRPC shortcut. 
	 * 
	 */		public class XMLRPCService extends XMLService implements IService
	{
		/**
		 * String that indicates the String type
		 */
		private static const TYPE_STRING:String = "string";
		/**
		 * String that indicates the integer type
		 */
		private static const TYPE_INT:String = "int";
		/**
		 * String that indicates the integer type
		 */
		private static const TYPE_I4:String = "i4";
		/**
		 * String that indicates the Number type
		 */
		private static const TYPE_DOUBLE:String = "double";
		/**
		 * String that indicates the Date type
		 */
		private static const TYPE_DATE:String = "dateTime.iso8601";
		/**
		 * String that indicates the Boolean type
		 */
		private static const TYPE_BOOLEAN:String = "boolean";
		/**
		 * String that indicates the Array type
		 */
		private static const TYPE_ARRAY:String = "array";
		/**
		 * String that indicates the Object type
		 */
		private static const TYPE_STRUCT:String = "struct";
		/**
		 * String that indicates the Base64 type
		 */
		private static const TYPE_BASE64:String = "base64";
		/**
		 * String that indicates the post method
		 */
		private var _method:String;

		
		/**
		 * @inheritDoc
		 * 
		 */		
		public function XMLRPCService( url:String = null, method:String = URLRequestMethod.POST )
		{
			super( url, method );
		}

		
		/**
		 * @inheritDoc
		 * 
		 */
		override public function sendAndLoad( ...args ):void
		{
			if ( args.length > 0 )
			{
				this._method = args.shift( ) as String;
				this._request.contentType = "text/xml";
				this._request.data = this.serialize( args );
				this._loader.load( this._request );
			}
			else
				throw new Error( "" );
		}

		
		/**
		 * @inheritDoc
		 * 
		 */			
		override protected function onResult( event:Event ):void
		{
			this.deserialize( this._loader.data );
		}

		
		/**
		 * Serializes the arguments
		 * 
		 */		
		private function serialize( arguments:Array ):XML
		{
			var myRequest:XML = <methodCall>
									<methodName>{this._method}</methodName>
								</methodCall>;
							
			var myParam:*;
			var myParams:XML = <params></params>;
			for ( var i:int = 0; i < arguments.length ; i++ )
			{
				myParam = arguments[ i ];
				myParams.appendChild( <param><value>{this.encodeObject( myParam )}</value></param> );
			}
			
			myRequest.insertChildAfter( myRequest.methodName, myParams );
			return myRequest;          
		}

		
		/**
		 * Deserializes the result of the XMLRPC call. Converts the XML result to 
		 * Flash objects.
		 * 
		 * @param	Data to deserialize
		 * 
		 */		
		private function deserialize( data:String ):void
		{
			// Set local properties
			var myEvent:ServiceEvent;
			
			var myData:XML = new XML( data.toString( ) );
			var myResult:XMLList = myData..param;
			var myFault:XMLList = myData..fault;
			
			// If the service responds with a valid response
			if ( myResult.toString( ) != "" && this.hasEventListener( ServiceEvent.RESULT ) )
			{
				myEvent = new ServiceEvent( ServiceEvent.RESULT );
				myEvent.data = this.decodeObject( myResult.value );
			}
			
			// Otherwise with an error
			else if ( this.hasEventListener( ServiceEvent.FAULT ) )
			{
				var myEncodedFault:Object = this.decodeObject( myFault.value );
			
				myEvent = new ServiceEvent( ServiceEvent.FAULT );
				myEvent.data = myEncodedFault;
				myEvent.errorCode = String( myEncodedFault.faultCode );
				myEvent.errorMessage = String( myEncodedFault.faultString );
			}
			
			// Dispatch event
			if ( myEvent )
				this.dispatchEvent( myEvent );
		}		

		
		/**
		 * Encodes an Flash Object to XMLRPC valid representation
		 * @param	Object to encode
		 * 
		 */
		private function encodeObject( value:* ):*
		{
			var myResult:XMLList;

			// Encode String
			if ( value is String )
				myResult = this.encodeString( value as String );

			// Encode Double
			else if ( value is Number )
			{
				// Encode integer
				if ( Math.floor( value ) == value)
					myResult = this.encodeInteger( value );

				// Encode Double
				else
					myResult = this.encodeDouble( value );
			}
				
			// Encode Array
			else if (value is Array)
				myResult = this.encodeArray( value );
			
			// Encode Boolean
			else if (value is Boolean)
				myResult = this.encodeBoolean( value );
				
			else if ( value is Date )
				myResult = this.encodeDate( value );

			// Encode ByteArray
			else if ( value is ByteArray )
				myResult = this.encodeBase64( value );
				
			// Encode Object
			else if (value is Object)
				myResult = this.encodeStruct( value );

			return myResult;
		}

		
		protected function decodeObject(value:*):*
		{
			var myResult:*;
			var myType:String = value.children( ).name( );

			switch ( myType )
			{
				// Decode string
				case TYPE_STRING: 
					myResult = this.decodeString( value[ TYPE_STRING ] ); 
					break;

				// Decode double
				case TYPE_DOUBLE: 
					myResult = this.decodeDouble( value[ TYPE_DOUBLE ] ); 
					break;	
					
				// Decode integer
				case TYPE_INT: 
					myResult = this.decodeInteger( value[ TYPE_INT ] ); 
					break;
					
				// Decode integer 
				case TYPE_I4: 
					myResult = this.decodeInteger( value[ TYPE_I4 ] ); 
					break; 
				
				// Decode Boolean
				case TYPE_BOOLEAN:
					myResult = this.decodeBoolean( value[ TYPE_BOOLEAN ] ); 
					break; 		

				// Decode date
				case TYPE_DATE:
					myResult = this.decodeDate( value[ TYPE_DATE ] ); 
					break; 		
									
				// Decode Array
				case TYPE_ARRAY:
					myResult = this.decodeArray( value[ TYPE_ARRAY ] ); 
					break;

				// Decode structure
				case TYPE_STRUCT:
					myResult = this.decodeStruct( value[ TYPE_STRUCT ] ); 
					break;		
					
				case TYPE_BASE64:
					myResult = this.decodeBase64( value[ TYPE_BASE64] );
					break;			
			}
			
			return myResult;
		}

		
		/**
		 * Encodes a String to an XMLRPC valid representation
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function encodeString( value:String ):XMLList
		{
			return new XMLList( "<" + TYPE_STRING + ">" + value + "</" + TYPE_STRING + ">" );
		}

		
		/**
		 * Encodes a integer to an XMLRPC valid representation
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function encodeInteger( value:int ):XMLList
		{
			return new XMLList( "<" + TYPE_INT + ">" + value + "</" + TYPE_INT + ">" );
		}

		
		/**
		 * Encodes a Number to an XMLRPC valid representation
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function encodeDouble( value:Number ):XMLList
		{
			return new XMLList( "<" + TYPE_DOUBLE + ">" + value + "</" + TYPE_DOUBLE + ">" );
		}

		
		/**
		 * Encodes a Boolean to an XMLRPC valid representation
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function encodeBoolean( value:Boolean ):XMLList
		{
			var myValue:String = value ? "1" : "0";
			return new XMLList( "<" + TYPE_BOOLEAN + ">" + myValue + "</" + TYPE_BOOLEAN + ">" );
		}

		
		/**
		 * Encodes a Date to an XMLRPC valid representation
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function encodeDate( date:Date ):XMLList
		{
			var myYear:String = date.getFullYear( ).toString( );
			var myMonth:String = date.getMonth( ) + 1 < 10 ? "0" + ( date.getMonth( ) + 1 ) : ( date.getMonth( ) + 1 ).toString( ); 
			var myDay:String = date.getDate( ) < 10 ? "0" + date.getDate( ) : date.getDate( ).toString( );

			var myHours:String = date.getHours( ) < 10 ? "0" + date.getHours( ) : date.getHours( ).toString( );
			var myMinutes:String = date.getMinutes( ) < 10 ? "0" + date.getMinutes( ) : date.getMinutes( ).toString( ); 
			var mySeconds:String = date.getSeconds( ) < 10 ? "0" + date.getSeconds( ) : date.getSeconds( ).toString( );

			var myDate:String = myYear + myMonth + myDay + "T" + myHours + ":" + myMinutes + ":" + mySeconds;	
			return new XMLList( "<" + TYPE_DATE + ">" + myDate + "</" + TYPE_DATE + ">" );
		}

		
		/**
		 * Encodes an Array to an XMLRPC valid representation
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function encodeArray( value:Array ):XMLList
		{
			var myValue:XML = <array></array>;
			var myData:XML = <data></data>;

			for (var i:int; i < value.length ; i++)
				myData.appendChild( <value>{encodeObject( value[i] )}</value> );
			myValue.appendChild( myData );

			return new XMLList( myValue );
		}

		
		/**
		 * Encodes an Object to an XMLRPC valid representation
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function encodeStruct( value:* ):XMLList
		{
			var myValue:XML = <struct></struct>;
			for ( var i:* in value )
				myValue.appendChild( <member><name>{i}</name><value>{encodeObject( value[i] )}</value></member> );

			return new XMLList( myValue );
		}

		
		/**
		 * Encodes a ByteArray to an XMLRPC valid representation
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function encodeBase64( value:ByteArray ):XMLList
		{
			var myValue:String = Base64.encodeByteArray( value );
			return new XMLList( "<" + TYPE_BASE64 + ">" + myValue + "</" + TYPE_BASE64 + ">" );
		}		

		
		/**
		 * Decodes an XML-RPC String representation to a valid Flash object
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function decodeString( string:XMLList ):String
		{
			return string.text( );
		}

		
		/**
		 * Decodes an XML-RPC integer representation to a valid Flash object
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function decodeInteger( int:XMLList ):Number
		{
			return new Number( int.text( ) );
		}

		
		/**
		 * Decodes an XML-RPC double representation to a valid Flash object
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function decodeDouble( double:XMLList ):Number
		{
			return Number( double.text( ) );
		}		

		
		/**
		 * Decodes an XML-RPC boolean representation to a valid Flash object
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function decodeBoolean( boolean:XMLList ):Boolean
		{
			return Boolean( parseInt( boolean.text( ) ) );		
		}

		
		/**
		 * Decodes an XML-RPC date representation to a valid Flash object
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function decodeDate( date:XMLList ):Date
		{
			var myFormattedDate:Date;
			if ( date.text( ).length( ) > 0 )
			{
				var mySplittedDate:Array = String( date.text( ) ).split( "T" );
	
				var myDate:String = mySplittedDate[ 0 ] as String;
				var myTime:Array = String( mySplittedDate[ 1 ] ).split( ":" );
			
				var myYear:int = parseInt( myDate.substr( 0, 4 ) );
				var myMonth:int = parseInt( myDate.substr( 4, 2 ) ) - 1;
				var myDay:int = parseInt( myDate.substr( 6, 2 ) );
	
				var myHours:int = int( myTime[ 0 ] );
				var myMinutes:int = int( myTime[ 1 ] );
				var mySeconds:int = int( myTime[ 2 ] );
			
				myFormattedDate = new Date( myYear, myMonth, myDay, myHours, myMinutes, mySeconds ); 
			}	
			
			return myFormattedDate;	
		}

		
		/**
		 * Decodes an XML-RPC array representation to a valid Flash object
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function decodeArray( array:XMLList ):Array
		{
			var myArray:Array = new Array;
			for each ( var myValue:* in array.data.value )
				myArray.push( this.decodeObject( myValue ) );
			
			return myArray;
		}

		
		/**
		 * Decodes an XML-RPC Structure representation to a valid Flash object
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function decodeStruct( struct:XMLList ):Object
		{
			var myName:String;
			var myValue:*;
			
			var myStructure:Object = new Object;
			for each ( var member:* in struct.member )
			{
				myName = member.name.text( );
				myValue = member.value;
				
				myStructure[ myName ] = this.decodeObject( myValue );
			}

			return myStructure;
		}

		
		/**
		 * Decodes an XML-RPC base64 representation to a valid Flash object
		 * @param	Original value
		 * @param	XMLList that indicates the encoded result
		 * 
		 */
		protected function decodeBase64( base64:String ):ByteArray
		{
			return Base64.decodeToByteArray( base64 );
		}		
	}
}